分类
联系方式
  1. 新浪微博
  2. E-mail

QuteBrowser 的命令模式架构实现

介绍

QuteBrowser、Vim、Emacs 的共同核心都是运行时的命令模式,在 QuteBrowser 中是如何实现的呢?在本文中进行梳理。

cmdutils

cmdutils 是一个用于方便命令注册的工具模块,位于 qutebrowser\api\cmdutils.py。

从源码注释中可以得知:

QuteBrowser 中有 Function 的概念,向用户暴露交互式的指令。

通过以下代码可以很容易创建一条指令:

from qutebrowser.api import cmdutils

@cmdutils.register(...)
def foo():
    ...

命令参数能通过检查函数自动推导出来。

函数参数的类型是根据参数默认值进行推导的,比如 foo=True 会转换为 -f 或者 --foo,展示在 QuteBrowser 的命令行中。

也能够通过类型声明指定参数类型:def foo(bar: int, baz=True)

支持的类型:

  • 一个可调用对象(``int', ``float', etc.)。被调用以验证/转换值。
  • Python 枚举
  • Python Union 多类型,比如 Union[str, int]

register 类

该类负责将命令注册到命令注册表中。

__call__ 方法

该方法执行实际注册操作:

def __call__(self, func: _CmdHandlerType) -> _CmdHandlerType:
    """Register the command before running the function.
    Args:
        func: The function to be decorated.

    Return:
        The original function (unmodified).
    """
    if self._name is None:
        name = func.__name__.lower().replace('_', '-')
    else:
        assert isinstance(self._name, str), self._name
        name = self._name

    cmd = command.Command(
        name=name,
        instance=self._instance,
        handler=func,
        **self._kwargs,
    )
    cmd.register()

    # ……

    return func

其中:

  • 参数中的 func 是被装饰器注解的函数
  • 这个方法只是将 func 拿过来,封装成一个 Command 实例,然后调用 Command.register()(内部工作肯定是向注册表中注册)
  • 最后将该方法原原本本地返回出去
  • 同时还有一个对函数名规范化的过程,将均转为小写,并将下划线转为横杠

Command 类

这是 QuteBrowser 命令所对应的实体。

属性

包含以下属性:

Command 类属性
属性 类型 说明
name str 命令名称
maxsplit int 对命令行进行分割的最大数量,或 None
deprecated str 描述为何废弃该命令
desc str 命令描述
handler 函数 命令对应的函数对象
debug bool 是否是调试命令,只在 --debug 时展示
parser 用于解析参数的 ArgumentParser
flags_with_args 一个带有参数的标志的列表。
no_cmd_split bool 如果为真,用于分割子命令的 ';;' 将被忽略。
backend 该命令适用于哪个后端(如果适用于两个后端则为无)。两者都适用)
no_replace_variables 不要替换像 {url} 这样的变量。
modes 该命令运行在的模式
_qute_args @cmdutils. 参数中保存的数据
_count 该命令的计数设置。
_instance 绑定 "self"的对象。
_scope 要在对象注册表中获取 _instance 的范围。

构造函数

这里以上一节中,实际调用代码为例:

cmd = command.Command(
        name=name,
        instance=self._instance,
        handler=func,
        **self._kwargs,
)
cmd.register()

其中,最后一个参数 kwargs 需要关注,举几个实际调用的例子:

maxiee kargs = {'modes': [<KeyMode.command: 3>, <KeyMode.prompt: 5>]}
maxiee kargs = {'modes': [<KeyMode.command: 3>, <KeyMode.prompt: 5>], 'deprecated': "Use :rl-rubout ' ' instead."}  
maxiee kargs = {'debug': True, 'maxsplit': 0, 'no_cmd_split': True}
maxiee kargs = {'modes': [<KeyMode.caret: 8>]}
maxiee kargs = {'scope': 'window'}

所以完整的构造参数,需要结合 kargs 一起来看。

构造函数里的部分核心逻辑:

  • 如果 mode 没有执行,采用默认值 usertypes.KeyMode
  • 其它的基本上都是保存到属性中

register 方法

实现如下:

def register(self):
    """Register this command in objects.commands."""
    log.commands.vdebug(  # type: ignore[attr-defined]
        "Registering command {} (from {}:{})".format(
            self.name, self.handler.__module__, self.handler.__qualname__))
    if self.name in objects.commands:
        raise ValueError("{} is already registered!".format(self.name))
    objects.commands[self.name] = self

可以看到,把 Command 实例往 objects 中一存完事。 关于 objects 导入方式为:

from qutebrowser.misc import objects

该模块是一个保存全局单例用的,objects 内 commands 对应代码如下:

commands: Dict[str, 'command.Command'] = {}